using System.Net;
using System.Net.Sockets;

namespace Pinwheel.Kernel
{
    public class ClientSocket : IClientSocket, IDisposable
    {
        public const int DefaultBufferSize = 128 * 1024;    // 128KB
        
        int m_bufferSize;                                   // 缓冲区大小
        
        Socket m_socket;                                    // Socket对象
        IPEndPoint m_point;                                 // 远程地址
        
        ISocketSession m_session;                           // Socket令牌
        ISocketListener m_listener;                         // Socket监听器
        // ISocketSessionProvider m_sessionProvider;           // Socket令牌提供器
        
        public bool IsConnected => null! != m_socket && m_socket.Connected;
        public ISocketSession Session => m_session;
        
        
        #region Constructors
        
        public ClientSocket(ISocketListener listener) : this(listener, ProtocolType.Tcp, DefaultBufferSize)
        {
        }
        
        public ClientSocket(ISocketListener listener, int size) : this(listener, ProtocolType.Tcp, size)
        {
        }
        
        public ClientSocket(ISocketListener listener, bool dual) : this(listener, ProtocolType.Tcp, DefaultBufferSize, dual)
        {
        }
        
        public ClientSocket(ISocketListener listener, int size, bool dual) : this(listener, ProtocolType.Tcp, size, dual)
        {
        }
        
        public ClientSocket(ISocketListener listener, ProtocolType protocolType, int size, bool dual = false)
        {
            m_listener = listener;
            
            var addressFamily = dual ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork;
            m_socket = new Socket(addressFamily, SocketType.Stream, protocolType);
            if (dual) m_socket.DualMode = dual;
            m_bufferSize = size;
            
            var receiveEventArgs = new SocketAsyncEventArgs();
            receiveEventArgs.Completed += OnIOCompleted;
            
            var sendEventArgs = new SocketAsyncEventArgs();
            sendEventArgs.Completed += OnIOCompleted;
            
            m_session = new SocketSession(this, listener, receiveEventArgs, sendEventArgs, size);
        }

        #endregion
        
        
        #region Connect
        
        public void Connect(string host, int port)
        {
            m_point = new IPEndPoint(IPAddress.Parse(host), port);
            m_session.ReceiveEventArgs.RemoteEndPoint = m_point;
            bool willRaiseEvent = m_socket.ConnectAsync(m_session.ReceiveEventArgs);
            if (!willRaiseEvent)
            {
                ProcessConnect(m_session.ReceiveEventArgs);
            }
        }
        
        public void Connect(IPAddress address, int port)
        {
            m_point = new IPEndPoint(address, port);
            m_session.ReceiveEventArgs.RemoteEndPoint = m_point;
            bool willRaiseEvent = m_socket.ConnectAsync(m_session.ReceiveEventArgs);
            if (!willRaiseEvent)
            {
                ProcessConnect(m_session.ReceiveEventArgs);
            }
        }

        private void ProcessConnect(SocketAsyncEventArgs socketEvent)
        {
            Assert.Check(null != m_session, "Try process connect with null session.");
            if (socketEvent.SocketError == SocketError.Success)
            {
                var socket = socketEvent.ConnectSocket;
                m_session.Connect(socket);
                StartReceive(socketEvent);
            }
            else
            {
                m_session.ConnectFailed();
                Close();
            }
        }

        #endregion
        
        
        private void OnIOCompleted(object sender, SocketAsyncEventArgs socketEvent)
        {
            switch (socketEvent.LastOperation)
            {
                case SocketAsyncOperation.Connect:
                    ProcessConnect(socketEvent);
                    break;
                case SocketAsyncOperation.Receive:
                    ProcessReceive(socketEvent);
                    break;
                case SocketAsyncOperation.Send:
                    ProcessSend(socketEvent);
                    break;
            }
        }
        
        
        #region Receive

        public void StartReceive(SocketAsyncEventArgs socketEvent)
        {
            try
            {
                var socketBuffer = m_session.Received;
                socketEvent.SetBuffer(socketBuffer.Buffer, socketBuffer.Tail, socketBuffer.WriteCount);
                bool willRaiseEvent = m_socket.ReceiveAsync(socketEvent);
                if (!willRaiseEvent)
                {
                    ProcessReceive(socketEvent);
                }
            }
            catch (SocketException e)
            {
                Log.Exception(e);
                Close();
            } 
        }

        private void ProcessReceive(SocketAsyncEventArgs socketEvent)
        {
            // 如果接收到的字节数为 0，表示远程端已经关闭连接
            if (socketEvent.SocketError != SocketError.Success || socketEvent.BytesTransferred <= 0)
            {
                Close();
                return;
            }
            
            Socket socket = socketEvent.AcceptSocket;
            Assert.Check(null != socket, "[Socket] ProcessReceive socket is null.");
            m_session.Receive(socket);
            
            StartReceive(socketEvent);
        }

        #endregion
        
        
        #region Send
        
        public void Send(byte[] data)
        {
            m_session.Send(data);
        }

        public void StartSend(SocketAsyncEventArgs socketEvent)
        {
            Socket socket = socketEvent.AcceptSocket;
            Assert.Check(null != socket, "[Socket] Send socket is null.");
            
            bool willRaiseEvent = socket.SendAsync(socketEvent);
            if (!willRaiseEvent)
            {
                ProcessSend(socketEvent);
            }
        }
        
        public void ProcessSend(SocketAsyncEventArgs socketEvent)
        {
            if (socketEvent.SocketError != SocketError.Success)
            {
                Close();
                return;
            }
            
            Assert.Check(null != m_session, $"[Socket] ProcessSend with null session.");
            m_session.Sended(socketEvent.Count);
            
            if (m_session.Prepare.ReadCount > 0)
                StartSend(socketEvent);
        }
        
        #endregion
        
        public void Disconnect() => Close();

        public void Close()
        {
            if (!m_socket.Connected) return;
            // Assert.Check(m_socket.Connected, "[Socket] Try close socket without connect.");
            Assert.Check(null != m_session, "[Socket] Close socket with null session.");
            try
            {
                m_socket.Shutdown(SocketShutdown.Both);
            }
            catch (SocketException e)
            {
                Log.Exception(e);
            }
            finally
            {
                m_session.Close(m_socket);
                m_socket.Close();
            }
        }
        
        public void Dispose()
        {
            Close();
            m_socket.Dispose();
        }
    }
}